feat(byok): add @tanstack/ai-byok bring-your-own-key toolkit#906
feat(byok): add @tanstack/ai-byok bring-your-own-key toolkit#906tombeckenham wants to merge 12 commits into
Conversation
Client keyring, per-provider request headers, and stateless server helpers that never persist or log provider keys. Keys live client-side and travel in an x-tanstack-byok-<provider> header, never the request body or message history. - @tanstack/ai-byok: provider registry, byokHeaders, pluggable storage (memory default, opt-in plaintext localStorage), validateKey - /react: <ByokProvider storage>, useByok, drop-in <ByokKeyManager> (last-4 display only) - /server: getByokKey (header-only, never logged), byokMissing (typed error), scrubSecrets/maskKey Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (6)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughThis PR adds ChangesCore BYOK package
ts-react-chat example integration
End-to-end BYOK route and tests
Estimated code review effort: 4 (Complex) | ~75 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🚀 Changeset Version Preview3 package(s) bumped directly, 0 bumped as dependents. 🟨 Minor bumps
|
|
View your CI Pipeline Execution ↗ for commit 1c9cdc3
☁️ Nx Cloud last updated this comment at |
|
View your CI Pipeline Execution ↗ for commit c9d0226
☁️ Nx Cloud last updated this comment at |
@tanstack/ai
@tanstack/ai-acp
@tanstack/ai-angular
@tanstack/ai-anthropic
@tanstack/ai-bedrock
@tanstack/ai-byok
@tanstack/ai-claude-code
@tanstack/ai-client
@tanstack/ai-code-mode
@tanstack/ai-code-mode-skills
@tanstack/ai-codex
@tanstack/ai-devtools-core
@tanstack/ai-elevenlabs
@tanstack/ai-event-client
@tanstack/ai-fal
@tanstack/ai-gemini
@tanstack/ai-grok
@tanstack/ai-grok-build
@tanstack/ai-groq
@tanstack/ai-isolate-cloudflare
@tanstack/ai-isolate-node
@tanstack/ai-isolate-quickjs
@tanstack/ai-mcp
@tanstack/ai-mistral
@tanstack/ai-ollama
@tanstack/ai-openai
@tanstack/ai-opencode
@tanstack/ai-openrouter
@tanstack/ai-preact
@tanstack/ai-react
@tanstack/ai-react-ui
@tanstack/ai-sandbox
@tanstack/ai-sandbox-cloudflare
@tanstack/ai-sandbox-daytona
@tanstack/ai-sandbox-docker
@tanstack/ai-sandbox-local-process
@tanstack/ai-sandbox-sprites
@tanstack/ai-sandbox-vercel
@tanstack/ai-solid
@tanstack/ai-solid-ui
@tanstack/ai-svelte
@tanstack/ai-utils
@tanstack/ai-vue
@tanstack/ai-vue-ui
@tanstack/openai-base
@tanstack/preact-ai-devtools
@tanstack/react-ai-devtools
@tanstack/solid-ai-devtools
commit: |
Replace the plaintext localStorage tier with passkey-encrypted persistence (WebAuthn PRF -> HKDF -> AES-256-GCM ciphertext in IndexedDB), unwrapped on demand with a biometric/PIN tap. Fully client-side; protects at-rest, not live in-page XSS (documented). - passkeyStorage() + isPasskeyStorageSupported() feature detection - KeyringStorage gains optional `unlockable` + `warning` - ByokProvider: `locked`/`unlock` so unlockable storage never prompts on mount; hydrates on explicit unlock or first save - ByokKeyManager: unlock banner + storage-specific warning - memoryStorage() remains the default; no plaintext persistence exists Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion
- New /byok route: ByokProvider (passkey storage where supported, else
memory) + ByokKeyManager + a minimal chat that attaches byokHeaders(keys)
- New /api/byok-chat relay: reads the key via getByokKey(request, provider),
builds the adapter with create{Openai,Anthropic,Gemini}Chat(model, apiKey),
returns byokMissing() when absent — stateless, no persist/log
- passkeyStorage gains an rpId option; by default the passkey binds to the
current origin (no hardcoded/central domain)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New testing/e2e /byok route + /api/byok-chat relay + byok.spec.ts: - key rides in the x-tanstack-byok-openai header, is absent from the request body, streams the aimock response, and the manager shows only the last-4 - missing key → byokMissing 401 surfaced as an error, no answer produced Keyring is hydrated via a preloaded storage (the same load() path passkey storage uses), so the flow is deterministic without a live WebAuthn ceremony (passkey crypto + locked/unlock are covered by package unit tests). Adds an apiKeyOverride to the e2e createTextAdapter and a byok-masked test hook to ByokKeyManager. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…v-key alerts Replaces the standalone /byok demo page with front-page integration: - key icon in the model bar opens a dark-themed dialog for per-provider keys (last-4 only), matching the app theme - getEnvKeyStatus server fn reports which providers have a server env key (booleans only, never the value); the key icon shows an amber dot and a banner warns when the selected model's provider has no key - api/tanchat prefers a per-request BYOK header key over env per provider (withByok helper); connection attaches byokHeaders(keys) Removes the old /byok route + api.byok-chat.ts + nav link. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ple uses passkey
Persistent storage can now report presence without decrypting, so the UI
knows keys exist after a refresh:
- KeyringStorage gains optional peek() → { provider: last-4 }
- passkeyStorage stores an unencrypted provider→last-4 sidecar next to the
ciphertext and reads it via peek() with no unlock ceremony
- new KeyStatus 'locked'; ByokProvider peeks on mount to mark saved keys
locked (with last-4); unlock() promotes them to 'set' on decrypt
Example front page now uses passkey-encrypted storage when a platform
authenticator is available (else memory). The key dialog shows locked keys
with a lock + last-4 and an "Unlock saved keys" action; the model-bar
warning distinguishes "no key" from "saved but locked → Unlock".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… on byokMissing
The SSE adapter throws a generic HTTP error on non-2xx without the body, so
useChat's error can't identify the missing provider. withByok wires the
provider's fetchClient option to peek at the relay's byokMissing 401 body:
- withByok(getKeys, { onMissingKey }) attaches byokHeaders per request and
invokes onMissingKey(provider) when the relay returns byokMissing
- byokFetch is the lower-level fetch wrapper; both exported from root + /react
- root now re-exports isByokMissingBody / ByokMissingBody
Example: /api/tanchat returns byokMissing(provider) when it has no server env
key and no BYOK header (instead of a generic 500); the front page uses
withByok, and onMissingKey opens the (now controllable) key dialog focused on
that provider — or, if the key is saved-but-locked, calls unlock() instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (8)
packages/ai-byok/tests/react.test.tsx (1)
6-7: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMerge duplicate
../src/indextype imports.Static analysis flags
import/no-duplicatesfor the two separate type-only imports from../src/index.🧹 Proposed fix
-import type { Keyring } from '../src/index' -import type { KeyringStorage } from '../src/index' +import type { Keyring, KeyringStorage } from '../src/index'🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-byok/tests/react.test.tsx` around lines 6 - 7, The React test file has duplicate type-only imports from the same module, triggering import/no-duplicates. Update the import section in react.test.tsx to merge the Keyring and KeyringStorage type imports into a single import statement from ../src/index, keeping the existing type-only form and preserving the referenced symbols.Source: Linters/SAST tools
packages/ai-byok/tests/byok.test.ts (1)
1-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winFix import order/sort lint errors.
Static analysis flags:
byokHeaderNameout of alphabetical order in the named import on Line 2, and the type import on Line 3 should be ordered after the../src/client/passkeyimport.🧹 Proposed fix for import ordering
-import { byokFetch, byokHeaders, byokHeaderName, withByok } from '../src/index' -import type { Keyring } from '../src/index' -import { - byokMissing, - getByokKey, - isByokMissingBody, - maskKey, - scrubSecrets, -} from '../src/server' -import { memoryStorage } from '../src/client/storage' -import { - decryptKeyring, - deriveAesKey, - encryptKeyring, -} from '../src/client/passkey' +import { byokFetch, byokHeaderName, byokHeaders, withByok } from '../src/index' +import { + byokMissing, + getByokKey, + isByokMissingBody, + maskKey, + scrubSecrets, +} from '../src/server' +import { memoryStorage } from '../src/client/storage' +import { + decryptKeyring, + deriveAesKey, + encryptKeyring, +} from '../src/client/passkey' +import type { Keyring } from '../src/index'🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-byok/tests/byok.test.ts` around lines 1 - 16, The test file import ordering is violating lint rules: the named import from byok helpers has an out-of-order symbol, and the type-only import is placed before the client/passkey imports. Reorder the imports in byok.test.ts so the named specifiers in the ../src/index import are alphabetized and the type import for Keyring is moved to the correct position after the ../src/client/passkey import, keeping the existing symbols and groupings intact.Source: Linters/SAST tools
packages/ai-byok/src/client/passkey.ts (1)
299-322: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winGuard
ensureKeyagainst concurrent ceremonies.The doc comment promises "exactly one WebAuthn ceremony," but because the
cachedKey/cachedMetaassignment isawait-gated, two overlapping calls (e.g. asaveracing aload, or rapid double-save) both observe an empty cache, both callidbGet, and both run a full ceremony. In the register branch this can mint two passkeys with only the last record persisted, orphaning the other. Memoize the in-flight promise so concurrent callers share one ceremony.♻️ Sketch
let cachedKey: CryptoKey | null = null let cachedMeta: { credentialId: ArrayBuffer salt: Uint8Array<ArrayBuffer> } | null = null + let pending: Promise<{ + key: CryptoKey + credentialId: ArrayBuffer + salt: Uint8Array<ArrayBuffer> + }> | null = null async function ensureKey(): Promise<{ key: CryptoKey credentialId: ArrayBuffer salt: Uint8Array<ArrayBuffer> }> { if (cachedKey && cachedMeta) { return { key: cachedKey, ...cachedMeta } } - const existing = await idbGet(dbName) - ... - return { key: cachedKey, ...cachedMeta } + if (!pending) { + pending = (async () => { + // ...existing register/unlock logic, sets cachedKey/cachedMeta... + return { key: cachedKey!, ...cachedMeta! } + })().finally(() => { pending = null }) + } + return pending }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-byok/src/client/passkey.ts` around lines 299 - 322, The ensureKey helper currently allows overlapping calls to start separate WebAuthn ceremonies because cachedKey/cachedMeta are only populated after awaited work completes. Update ensureKey to memoize and share a single in-flight promise so concurrent callers (for example load and save, or rapid double-save) all await the same ceremony; reuse the existing cachedKey/cachedMeta fast path, but ensure the first unresolved call to ensureKey owns the idbGet/registerPasskey flow and later calls return that same promise until it settles.packages/ai-byok/src/client/validate.ts (1)
22-40: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winNo timeout on the validation fetch.
fetchhas noAbortSignal/timeout, so a stalled network or slow-to-respond provider endpoint leaves the caller'sawait(and, perbyok-context.tsx, the UI'svalidatingstate) hanging indefinitely with no way for the user to recover short of a page reload.⏱️ Proposed fix
export async function validateKey( provider: ProviderId, key: string, ): Promise<ValidationStatus> { const config = providerValidateConfig(provider) if (!config) return 'unsupported' - const response = await fetch(config.url, { - method: 'GET', - headers: config.headers(key), - }) + const response = await fetch(config.url, { + method: 'GET', + headers: config.headers(key), + signal: AbortSignal.timeout(10_000), + })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-byok/src/client/validate.ts` around lines 22 - 40, The validateKey() fetch call currently has no timeout, so a slow or stalled provider can leave the request and validating state hanging forever. Update validateKey() in validate.ts to use an AbortSignal with a timeout (for example via AbortController) around the fetch call, and ensure timeout aborts are handled as a validation failure instead of hanging. Keep the existing providerValidateConfig(), response handling, and ValidationStatus behavior intact.packages/ai-byok/src/server/byok-missing.ts (1)
39-44: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winType guard doesn't fully validate its asserted shape.
isByokMissingBodyonly checkserror.type, noterror.provider/error.message, yet it asserts the fullByokMissingBodyinterface (Line 4-10). Downstream,with-byok.tscallsonMissingKey(body.error.provider)(Line 23) trusting this is a validProviderId— an untrusted/malformed 401 body could pass this guard and hand a bad value to the caller's callback.♻️ Proposed fix to validate provider/message
import type { ProviderId } from '../shared/providers' +import { isProviderId } from '../shared/providers' ... export function isByokMissingBody(value: unknown): value is ByokMissingBody { if (typeof value !== 'object' || value === null) return false const { error } = value as { error?: unknown } if (typeof error !== 'object' || error === null) return false - return (error as { type?: unknown }).type === 'byok_missing' + const { type, provider, message } = error as { + type?: unknown + provider?: unknown + message?: unknown + } + return ( + type === 'byok_missing' && + typeof provider === 'string' && + isProviderId(provider) && + typeof message === 'string' + ) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-byok/src/server/byok-missing.ts` around lines 39 - 44, `isByokMissingBody` currently only verifies `error.type`, so it can accept malformed bodies while claiming to be `ByokMissingBody`. Update the guard in `byok-missing.ts` to fully validate the asserted shape by checking `error.provider` and `error.message` alongside `error.type`, ensuring `provider` matches a valid `ProviderId`-like string and `message` is a string before returning true. Keep the checks inside `isByokMissingBody` so `with-byok.ts` can safely call `onMissingKey(body.error.provider)` without trusting unvalidated data.packages/ai-byok/src/react/byok-key-manager.tsx (2)
101-107: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueNo confirmation before clearing a saved key.
A single click on "Clear" permanently removes the stored key with no undo/confirm step. Worth a lightweight confirm (or a two-step "Clear?" toggle) to prevent accidental deletion, though the user can always re-enter the key.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-byok/src/react/byok-key-manager.tsx` around lines 101 - 107, The Clear action in the byok-key-manager component triggers immediate key deletion with no safeguard, so add a lightweight confirmation step before calling clearKey(provider). Update the button behavior in ByokKeyManager to require a second explicit confirmation (or a confirm dialog/toggle) before clearing the saved key, while keeping the existing clearKey provider flow unchanged once confirmed.
88-88: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRedundant
'masked' in statuscheck.
hasKeyalready excludes only the'empty'state, and every non-'empty'KeyStatusvariant carriesmasked(perbyok-context.tsx'sKeyStatusunion). The extra narrowing is harmless but adds noise.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-byok/src/react/byok-key-manager.tsx` at line 88, Remove the redundant masked property check in byok-key-manager.tsx’s render condition: the hasKey guard already covers all non-empty KeyStatus values, so update the conditional around the masked UI branch to rely on hasKey alone and keep the logic aligned with the KeyStatus union defined in byok-context.tsx.packages/ai-byok/src/react.ts (1)
4-4: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueExporting the raw
ByokContextbypasses theuseByoksafety guard.Consumers can call
useContext(ByokContext)directly and getundefinedsilently instead of the descriptive error thrown byuseByok. Consider keepingByokContextinternal (only exported for testing) and steering public consumers exclusively touseByok.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-byok/src/react.ts` at line 4, The public re-export in react.ts is exposing ByokContext directly, which lets consumers bypass the safety checks in useByok and receive undefined silently. Remove ByokContext from the public export surface in react.ts, keep it internal or test-only in react/byok-context, and ensure consumers are directed to use the useByok hook (and ByokProvider) for all supported access paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/ts-react-chat/src/routes/api.tanchat.ts`:
- Around line 22-24: The import block in api.tanchat.ts violates the
import/order lint rule because the value import from BYOK config is placed
before the type-only import. Reorder the imports so the type import from
`@tanstack/ai-byok/server` stays grouped correctly and the value import
BYOK_PROVIDER_MAP/byokIdForProvider from `@/lib/byok-config` comes after it; keep
byokMissing and getByokKey with the other server imports and let the import
grouping in the module follow the linter’s expected order.
- Line 260: The generic type parameter name in byokAdapter violates the
TypeScript naming convention rule; rename M to a valid type-parameter identifier
that matches the required pattern, and update any references to that type
parameter within byokAdapter accordingly. Keep the change scoped to the
byokAdapter declaration and its uses so the lint error is resolved without
altering behavior.
In `@examples/ts-react-chat/src/routes/index.tsx`:
- Around line 37-39: The import order in the route module is violating the
import/order lint rule, so adjust the grouped and sorted imports near the top of
the file. Reorder the type-only and value imports involving ProviderId,
ByokKeyDialog, and byokIdForProvider/getEnvKeyStatus to match the project’s
import grouping conventions, then verify the file passes lint (or use the
auto-fix from the linter).
- Around line 877-882: Remove the unnecessary optional chaining in the
platformAuth calculation inside the index route component:
isPasskeyStorageSupported() already guards globalThis.PublicKeyCredential, so
update the PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
call to use it directly instead of ?. to satisfy no-unnecessary-condition while
keeping the existing runtime behavior unchanged.
In `@packages/ai-byok/src/client/passkey.ts`:
- Around line 57-64: The support check in isPasskeyStorageSupported() is unsafe
because it dereferences navigator.credentials.create directly, so it can throw
when navigator exists but credentials is undefined. Update the guard in this
function to safely probe navigator.credentials with optional chaining or an
explicit credentials existence check before accessing create, matching the safer
PublicKeyCredential probe so the fallback path can run.
In `@packages/ai-byok/src/react/byok-key-manager.tsx`:
- Around line 121-129: The password input placeholder in byok-key-manager.tsx is
using the raw provider id instead of the user-facing provider label, causing
inconsistent copy with the row header. Update the placeholder logic in the BYOK
key input to use the same label source as the header, namely
BYOK_PROVIDERS[provider].label, while preserving the existing Replace key…
behavior when hasKey is true.
- Around line 93-119: The `ByokKeyManager` action handlers are swallowing async
failures from `setKey` and `clearKey`, so rejected promises never reach the
user. Update the `Validate`, `Clear`, and form submit flows in `ByokKeyManager`
to handle rejections explicitly, similar to how `unlock()` surfaces
`unlockError`, and avoid clearing `draft` until `setKey` succeeds. Use the
existing `validateKey`, `clearKey`, and `setKey` entry points to attach error
handling and show the failure state instead of silently ignoring it.
In `@packages/ai-byok/src/shared/providers.ts`:
- Around line 62-70: The Gemini validation note in providers.ts is inaccurate:
the validate block for the gemini provider uses the x-goog-api-key header, not a
query parameter. Update the inline comment near the gemini validate
configuration to describe the actual header-based authentication used by the
validate URL, so the comment matches the behavior of the gemini provider
definition.
In `@testing/e2e/src/routes/byok.tsx`:
- Around line 4-12: The import order in byok.tsx violates the import/order rule
because the type-only `@tanstack/ai-byok/react` import is placed after the local
`@/components/ChatUI` import. Reorder the imports in the byok module so all
`@tanstack/ai-byok/react` imports (including the Keyring/KeyringStorage type
import) come before the local ChatUI import, keeping the existing grouped
structure intact.
---
Nitpick comments:
In `@packages/ai-byok/src/client/passkey.ts`:
- Around line 299-322: The ensureKey helper currently allows overlapping calls
to start separate WebAuthn ceremonies because cachedKey/cachedMeta are only
populated after awaited work completes. Update ensureKey to memoize and share a
single in-flight promise so concurrent callers (for example load and save, or
rapid double-save) all await the same ceremony; reuse the existing
cachedKey/cachedMeta fast path, but ensure the first unresolved call to
ensureKey owns the idbGet/registerPasskey flow and later calls return that same
promise until it settles.
In `@packages/ai-byok/src/client/validate.ts`:
- Around line 22-40: The validateKey() fetch call currently has no timeout, so a
slow or stalled provider can leave the request and validating state hanging
forever. Update validateKey() in validate.ts to use an AbortSignal with a
timeout (for example via AbortController) around the fetch call, and ensure
timeout aborts are handled as a validation failure instead of hanging. Keep the
existing providerValidateConfig(), response handling, and ValidationStatus
behavior intact.
In `@packages/ai-byok/src/react.ts`:
- Line 4: The public re-export in react.ts is exposing ByokContext directly,
which lets consumers bypass the safety checks in useByok and receive undefined
silently. Remove ByokContext from the public export surface in react.ts, keep it
internal or test-only in react/byok-context, and ensure consumers are directed
to use the useByok hook (and ByokProvider) for all supported access paths.
In `@packages/ai-byok/src/react/byok-key-manager.tsx`:
- Around line 101-107: The Clear action in the byok-key-manager component
triggers immediate key deletion with no safeguard, so add a lightweight
confirmation step before calling clearKey(provider). Update the button behavior
in ByokKeyManager to require a second explicit confirmation (or a confirm
dialog/toggle) before clearing the saved key, while keeping the existing
clearKey provider flow unchanged once confirmed.
- Line 88: Remove the redundant masked property check in byok-key-manager.tsx’s
render condition: the hasKey guard already covers all non-empty KeyStatus
values, so update the conditional around the masked UI branch to rely on hasKey
alone and keep the logic aligned with the KeyStatus union defined in
byok-context.tsx.
In `@packages/ai-byok/src/server/byok-missing.ts`:
- Around line 39-44: `isByokMissingBody` currently only verifies `error.type`,
so it can accept malformed bodies while claiming to be `ByokMissingBody`. Update
the guard in `byok-missing.ts` to fully validate the asserted shape by checking
`error.provider` and `error.message` alongside `error.type`, ensuring `provider`
matches a valid `ProviderId`-like string and `message` is a string before
returning true. Keep the checks inside `isByokMissingBody` so `with-byok.ts` can
safely call `onMissingKey(body.error.provider)` without trusting unvalidated
data.
In `@packages/ai-byok/tests/byok.test.ts`:
- Around line 1-16: The test file import ordering is violating lint rules: the
named import from byok helpers has an out-of-order symbol, and the type-only
import is placed before the client/passkey imports. Reorder the imports in
byok.test.ts so the named specifiers in the ../src/index import are alphabetized
and the type import for Keyring is moved to the correct position after the
../src/client/passkey import, keeping the existing symbols and groupings intact.
In `@packages/ai-byok/tests/react.test.tsx`:
- Around line 6-7: The React test file has duplicate type-only imports from the
same module, triggering import/no-duplicates. Update the import section in
react.test.tsx to merge the Keyring and KeyringStorage type imports into a
single import statement from ../src/index, keeping the existing type-only form
and preserving the referenced symbols.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b9c868f3-0974-4289-8a38-e41c277bf284
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (35)
.changeset/byok-package.mdexamples/ts-react-chat/package.jsonexamples/ts-react-chat/src/components/ByokKeyDialog.tsxexamples/ts-react-chat/src/lib/byok-config.tsexamples/ts-react-chat/src/routes/api.tanchat.tsexamples/ts-react-chat/src/routes/index.tsxknip.jsonpackages/ai-byok/README.mdpackages/ai-byok/package.jsonpackages/ai-byok/src/client/keyring.tspackages/ai-byok/src/client/passkey.tspackages/ai-byok/src/client/storage.tspackages/ai-byok/src/client/validate.tspackages/ai-byok/src/client/with-byok.tspackages/ai-byok/src/index.tspackages/ai-byok/src/react.tspackages/ai-byok/src/react/byok-context.tsxpackages/ai-byok/src/react/byok-key-manager.tsxpackages/ai-byok/src/react/use-byok.tspackages/ai-byok/src/server.tspackages/ai-byok/src/server/byok-missing.tspackages/ai-byok/src/server/get-byok-key.tspackages/ai-byok/src/server/scrub.tspackages/ai-byok/src/shared/providers.tspackages/ai-byok/tests/byok.test.tspackages/ai-byok/tests/react.test.tsxpackages/ai-byok/tsconfig.jsonpackages/ai-byok/vite.config.tstesting/e2e/README.mdtesting/e2e/package.jsontesting/e2e/src/lib/providers.tstesting/e2e/src/routeTree.gen.tstesting/e2e/src/routes/api.byok-chat.tstesting/e2e/src/routes/byok.tsxtesting/e2e/tests/byok.spec.ts
| import { byokMissing, getByokKey } from '@tanstack/ai-byok/server' | ||
| import { BYOK_PROVIDER_MAP, byokIdForProvider } from '@/lib/byok-config' | ||
| import type { ProviderId } from '@tanstack/ai-byok/server' |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Import ordering violates lint (import/order), will fail CI.
ESLint reports the value import of @/lib/byok-config (Line 23) must come after the @tanstack/ai type import. Run lint --fix to reorder.
🧰 Tools
🪛 ESLint
[error] 23-23: @/lib/byok-config import should occur after type import of @tanstack/ai
(import/order)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/ts-react-chat/src/routes/api.tanchat.ts` around lines 22 - 24, The
import block in api.tanchat.ts violates the import/order lint rule because the
value import from BYOK config is placed before the type-only import. Reorder the
imports so the type import from `@tanstack/ai-byok/server` stays grouped correctly
and the value import BYOK_PROVIDER_MAP/byokIdForProvider from `@/lib/byok-config`
comes after it; keep byokMissing and getByokKey with the other server imports
and let the import grouping in the module follow the linter’s expected order.
Source: Linters/SAST tools
| // BYOK: prefer a per-request key from the request header, falling back | ||
| // to the env-configured adapter. The key is read from the header only, | ||
| // never persisted or logged. | ||
| function byokAdapter<M extends string>( |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Rename type parameter M to satisfy naming-convention rule.
@typescript-eslint/naming-convention requires type params to match /^(T|T[A-Z][A-Za-z]+)$/. This is an [error]-level lint failure.
Proposed rename
- function byokAdapter<M extends string>(
+ function byokAdapter<TModel extends string>(
provider: ProviderId,
- m: M,
- byok: (model: M, apiKey: string) => AnyTextAdapter,
- env: (model: M) => AnyTextAdapter,
+ m: TModel,
+ byok: (model: TModel, apiKey: string) => AnyTextAdapter,
+ env: (model: TModel) => AnyTextAdapter,
): AnyTextAdapter {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function byokAdapter<M extends string>( | |
| function byokAdapter<TModel extends string>( | |
| provider: ProviderId, | |
| m: TModel, | |
| byok: (model: TModel, apiKey: string) => AnyTextAdapter, | |
| env: (model: TModel) => AnyTextAdapter, | |
| ): AnyTextAdapter { |
🧰 Tools
🪛 ESLint
[error] 260-260: Type Parameter name M must match the RegExp: /^(T|T[A-Z][A-Za-z]+)$/u
(@typescript-eslint/naming-convention)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/ts-react-chat/src/routes/api.tanchat.ts` at line 260, The generic
type parameter name in byokAdapter violates the TypeScript naming convention
rule; rename M to a valid type-parameter identifier that matches the required
pattern, and update any references to that type parameter within byokAdapter
accordingly. Keep the change scoped to the byokAdapter declaration and its uses
so the lint error is resolved without altering behavior.
Source: Linters/SAST tools
| import type { ProviderId } from '@tanstack/ai-byok/react' | ||
| import { ByokKeyDialog } from '@/components/ByokKeyDialog' | ||
| import { byokIdForProvider, getEnvKeyStatus } from '@/lib/byok-config' |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Import ordering violates lint (import/order), will fail CI.
ESLint flags the @tanstack/ai-byok/react type import (Line 37) and the @/components/ByokKeyDialog / @/lib/byok-config imports (Lines 38-39) as mis-ordered relative to the surrounding type imports. Run lint --fix.
🧰 Tools
🪛 ESLint
[error] 37-37: @tanstack/ai-byok/react type import should occur after import of @tanstack/ai-react-ui
(import/order)
[error] 38-38: @/components/ByokKeyDialog import should occur after type import of @/lib/model-selection
(import/order)
[error] 39-39: @/lib/byok-config import should occur after type import of @/lib/model-selection
(import/order)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/ts-react-chat/src/routes/index.tsx` around lines 37 - 39, The import
order in the route module is violating the import/order lint rule, so adjust the
grouped and sorted imports near the top of the file. Reorder the type-only and
value imports involving ProviderId, ByokKeyDialog, and
byokIdForProvider/getEnvKeyStatus to match the project’s import grouping
conventions, then verify the file passes lint (or use the auto-fix from the
linter).
Source: Linters/SAST tools
| const platformAuth = | ||
| isPasskeyStorageSupported() && | ||
| (await globalThis.PublicKeyCredential?.isUserVerifyingPlatformAuthenticatorAvailable().catch( | ||
| () => false, | ||
| )) | ||
| if (active) setStorage(platformAuth ? passkeyStorage() : memoryStorage()) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm isPasskeyStorageSupported checks for PublicKeyCredential before the optional chain is relied upon.
fd -t f 'passkey' packages/ai-byok/src | xargs -r rg -nP -C3 'isPasskeyStorageSupported|PublicKeyCredential'Repository: TanStack/ai
Length of output: 1664
Drop the optional chain here. isPasskeyStorageSupported() already guards globalThis.PublicKeyCredential, so ?. is unnecessary and triggers no-unnecessary-condition; remove it rather than weakening the runtime check.
🧰 Tools
🪛 ESLint
[error] 879-879: Unnecessary optional chain on a non-nullish value.
(@typescript-eslint/no-unnecessary-condition)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/ts-react-chat/src/routes/index.tsx` around lines 877 - 882, Remove
the unnecessary optional chaining in the platformAuth calculation inside the
index route component: isPasskeyStorageSupported() already guards
globalThis.PublicKeyCredential, so update the
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() call to use
it directly instead of ?. to satisfy no-unnecessary-condition while keeping the
existing runtime behavior unchanged.
Source: Linters/SAST tools
| export function isPasskeyStorageSupported(): boolean { | ||
| return ( | ||
| typeof globalThis !== 'undefined' && | ||
| typeof globalThis.PublicKeyCredential !== 'undefined' && | ||
| typeof globalThis.navigator !== 'undefined' && | ||
| typeof globalThis.navigator.credentials.create === 'function' | ||
| ) | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== packages/ai-byok/src/client/passkey.ts =="
sed -n '1,120p' packages/ai-byok/src/client/passkey.ts
echo
echo "== call sites for isPasskeyStorageSupported =="
rg -n "isPasskeyStorageSupported\(" -S .
echo
echo "== any direct navigator.credentials usage in relevant files =="
rg -n "navigator\.credentials|PublicKeyCredential" packages examples -SRepository: TanStack/ai
Length of output: 5666
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== examples/ts-react-chat/src/routes/index.tsx around support check =="
sed -n '860,890p' examples/ts-react-chat/src/routes/index.tsx
echo
echo "== README mentions of isPasskeyStorageSupported =="
sed -n '30,120p' packages/ai-byok/README.mdRepository: TanStack/ai
Length of output: 4687
Guard navigator.credentials before accessing create.
isPasskeyStorageSupported() can throw when navigator exists but navigator.credentials is undefined, so the support check itself becomes unsafe and the memory fallback never runs. Use optional chaining here, like the downstream PublicKeyCredential?. probe. packages/ai-byok/src/client/passkey.ts:57-64
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-byok/src/client/passkey.ts` around lines 57 - 64, The support
check in isPasskeyStorageSupported() is unsafe because it dereferences
navigator.credentials.create directly, so it can throw when navigator exists but
credentials is undefined. Update the guard in this function to safely probe
navigator.credentials with optional chaining or an explicit credentials
existence check before accessing create, matching the safer PublicKeyCredential
probe so the fallback path can run.
| <div style={styles.actions}> | ||
| <button | ||
| type="button" | ||
| style={styles.button} | ||
| onClick={() => void validateKey(provider)} | ||
| > | ||
| Validate | ||
| </button> | ||
| <button | ||
| type="button" | ||
| style={styles.button} | ||
| onClick={() => void clearKey(provider)} | ||
| > | ||
| Clear | ||
| </button> | ||
| </div> | ||
| </div> | ||
| ) : null} | ||
|
|
||
| <form | ||
| style={styles.inputRow} | ||
| onSubmit={(event) => { | ||
| event.preventDefault() | ||
| if (!draft) return | ||
| void setKey(provider, draft) | ||
| setDraft('') | ||
| }} |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Unhandled rejections on setKey/clearKey.
Both calls are fired with void and no .catch, unlike the unlock() handler which surfaces failures via unlockError. If setKey/clearKey reject (e.g. passkey encryption failure, WebAuthn ceremony cancellation, IndexedDB quota errors), the failure is silently swallowed — the input clears (line 118) even though the save may not have persisted, and the user gets no indication anything went wrong.
🐛 Proposed fix to surface save/clear errors
+ const [rowError, setRowError] = useState<string | null>(null)
+
return (
<div style={styles.row}>
...
<button
type="button"
style={styles.button}
- onClick={() => void clearKey(provider)}
+ onClick={() =>
+ clearKey(provider).catch((error: unknown) =>
+ setRowError(error instanceof Error ? error.message : String(error)),
+ )
+ }
>
Clear
</button>
...
<form
onSubmit={(event) => {
event.preventDefault()
if (!draft) return
- void setKey(provider, draft)
- setDraft('')
+ setKey(provider, draft)
+ .then(() => setDraft(''))
+ .catch((error: unknown) =>
+ setRowError(error instanceof Error ? error.message : String(error)),
+ )
}}
>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-byok/src/react/byok-key-manager.tsx` around lines 93 - 119, The
`ByokKeyManager` action handlers are swallowing async failures from `setKey` and
`clearKey`, so rejected promises never reach the user. Update the `Validate`,
`Clear`, and form submit flows in `ByokKeyManager` to handle rejections
explicitly, similar to how `unlock()` surfaces `unlockError`, and avoid clearing
`draft` until `setKey` succeeds. Use the existing `validateKey`, `clearKey`, and
`setKey` entry points to attach error handling and show the failure state
instead of silently ignoring it.
| <input | ||
| type="password" | ||
| autoComplete="off" | ||
| spellCheck={false} | ||
| placeholder={hasKey ? 'Replace key…' : `Paste ${provider} key…`} | ||
| value={draft} | ||
| onChange={(event) => setDraft(event.target.value)} | ||
| style={styles.input} | ||
| /> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Placeholder shows raw provider id instead of label.
Paste ${provider} key… uses the raw id (e.g. openai) while the row header above shows BYOK_PROVIDERS[provider].label. Minor inconsistency in user-facing copy.
✏️ Proposed fix
- placeholder={hasKey ? 'Replace key…' : `Paste ${provider} key…`}
+ placeholder={
+ hasKey ? 'Replace key…' : `Paste ${BYOK_PROVIDERS[provider].label} key…`
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <input | |
| type="password" | |
| autoComplete="off" | |
| spellCheck={false} | |
| placeholder={hasKey ? 'Replace key…' : `Paste ${provider} key…`} | |
| value={draft} | |
| onChange={(event) => setDraft(event.target.value)} | |
| style={styles.input} | |
| /> | |
| <input | |
| type="password" | |
| autoComplete="off" | |
| spellCheck={false} | |
| placeholder={ | |
| hasKey ? 'Replace key…' : `Paste ${BYOK_PROVIDERS[provider].label} key…` | |
| } | |
| value={draft} | |
| onChange={(event) => setDraft(event.target.value)} | |
| style={styles.input} | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-byok/src/react/byok-key-manager.tsx` around lines 121 - 129, The
password input placeholder in byok-key-manager.tsx is using the raw provider id
instead of the user-facing provider label, causing inconsistent copy with the
row header. Update the placeholder logic in the BYOK key input to use the same
label source as the header, namely BYOK_PROVIDERS[provider].label, while
preserving the existing Replace key… behavior when hasKey is true.
| gemini: { | ||
| id: 'gemini', | ||
| label: 'Google Gemini', | ||
| validate: { | ||
| // Gemini authenticates the models list via a query param, not a header. | ||
| url: 'https://generativelanguage.googleapis.com/v1beta/models', | ||
| headers: (key) => ({ 'x-goog-api-key': key }), | ||
| }, | ||
| }, |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the target file and nearby context.
sed -n '1,140p' packages/ai-byok/src/shared/providers.ts
# Search for Gemini-related validation/request code and comments.
rg -n "x-goog-api-key|generativelanguage.googleapis.com|query param|Gemini authenticates|validate:" packages/ai-byok/src -SRepository: TanStack/ai
Length of output: 5671
🌐 Web query:
Gemini API x-goog-api-key header docs models list query parameter header documentation
💡 Result:
To authenticate requests to the Gemini API, Google recommends passing your API key in the x-goog-api-key HTTP header rather than including it as a query parameter in the URL [1][2]. This practice improves security by preventing the API key from being exposed in logs or browser history [2]. Documentation for the models list endpoint indicates it is available via a GET request to the following path: GET https://generativelanguage.googleapis.com/v1beta/models To list the available models, you must include the x-goog-api-key header in your request [1][3]. While some legacy examples may still show the API key passed as a?key= query parameter [4][5], the x-goog-api-key header is the preferred, more secure method [2]. Example using cURL: curl https://generativelanguage.googleapis.com/v1beta/models \ -H 'Content-Type: application/json' \ -H "x-goog-api-key: YOUR_API_KEY" The models.list method supports the following optional query parameters to manage pagination [3]: pageSize (integer): The maximum number of models to return per page (default is 50, maximum is 1000) [3]. pageToken (string): A token received from a previous call to retrieve the next page of results [3].
Citations:
- 1: https://ai.google.dev/api
- 2: Include api key in header instead of in the URL google-gemini/gemini-cli#4240
- 3: https://ai.google.dev/api/models?hl=hi
- 4: https://github.com/google-gemini/cookbook/blob/e29e3a7d/quickstarts/rest/Models_REST.ipynb
- 5: https://github.com/google-gemini/cookbook/blob/main/quickstarts/Authentication.ipynb
Fix the Gemini validation comment The note says “query param,” but this request sends x-goog-api-key in a header; update the comment to match the actual auth mechanism.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-byok/src/shared/providers.ts` around lines 62 - 70, The Gemini
validation note in providers.ts is inaccurate: the validate block for the gemini
provider uses the x-goog-api-key header, not a query parameter. Update the
inline comment near the gemini validate configuration to describe the actual
header-based authentication used by the validate URL, so the comment matches the
behavior of the gemini provider definition.
| import { | ||
| ByokKeyManager, | ||
| ByokProvider, | ||
| byokHeaders, | ||
| memoryStorage, | ||
| useByok, | ||
| } from '@tanstack/ai-byok/react' | ||
| import { ChatUI } from '@/components/ChatUI' | ||
| import type { Keyring, KeyringStorage } from '@tanstack/ai-byok/react' |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix import order flagged by ESLint.
Static analysis flags the @tanstack/ai-byok/react type import (Line 12) as needing to precede the @/components/ChatUI import (Line 11) per import/order. This will fail the lint gate.
🔧 Proposed fix
import {
ByokKeyManager,
ByokProvider,
byokHeaders,
memoryStorage,
useByok,
} from '`@tanstack/ai-byok/react`'
+import type { Keyring, KeyringStorage } from '`@tanstack/ai-byok/react`'
import { ChatUI } from '`@/components/ChatUI`'
-import type { Keyring, KeyringStorage } from '`@tanstack/ai-byok/react`'📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { | |
| ByokKeyManager, | |
| ByokProvider, | |
| byokHeaders, | |
| memoryStorage, | |
| useByok, | |
| } from '@tanstack/ai-byok/react' | |
| import { ChatUI } from '@/components/ChatUI' | |
| import type { Keyring, KeyringStorage } from '@tanstack/ai-byok/react' | |
| import { | |
| ByokKeyManager, | |
| ByokProvider, | |
| byokHeaders, | |
| memoryStorage, | |
| useByok, | |
| } from '`@tanstack/ai-byok/react`' | |
| import type { Keyring, KeyringStorage } from '`@tanstack/ai-byok/react`' | |
| import { ChatUI } from '`@/components/ChatUI`' |
🧰 Tools
🪛 ESLint
[error] 12-12: @tanstack/ai-byok/react type import should occur before import of @/components/ChatUI
(import/order)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@testing/e2e/src/routes/byok.tsx` around lines 4 - 12, The import order in
byok.tsx violates the import/order rule because the type-only
`@tanstack/ai-byok/react` import is placed after the local `@/components/ChatUI`
import. Reorder the imports in the byok module so all `@tanstack/ai-byok/react`
imports (including the Keyring/KeyringStorage type import) come before the local
ChatUI import, keeping the existing grouped structure intact.
Source: Linters/SAST tools
The BYOK header prefix, IndexedDB name, passkey relying-party name, and HKDF label all hardcoded "tanstack". None of these need the vendor name: the header is a private protocol between this package's own client and server (both resolve it via byokHeaderName), and the storage identifiers are already overridable per instance. Neutral defaults let the toolkit read as reusable rather than TanStack-specific. - header prefix: x-tanstack-byok- -> x-byok- - IndexedDB default: tanstack-byok -> byok - passkey rpName: "TanStack AI BYOK" -> "BYOK" - HKDF info label: tanstack-byok:keyring:v1 -> byok:keyring:v1 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirrors withByok (which targets the connection transport) for the fetcher transport used by useChat/useGeneration. Hands the fetcher body fresh BYOK headers + a missing-key-aware fetch, covering both a plain fetch call and a TanStack Start server function (via call-site headers). Keys still travel in the x-byok-<provider> header, never the body. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Adds
@tanstack/ai-byok— a bring-your-own-key toolkit for TanStack AI. Users supply their own provider keys; the library collects them client-side, attaches them per-request in a header, and uses them server-side without ever persisting or logging them.First principle: never be a custodian. Keys live client-side (the browser is the system of record). The server piece is a stateless pass-through — it reads the key off the incoming request header, hands it to the adapter for one call, and never writes it to a DB, cache, log, or observability stream. No central endpoint is baked in, so third parties self-host their own relay.
Keys travel in
x-tanstack-byok-<provider>— never the request body or message history — so they stay out of persisted conversations and the event/observability stream.Package layout
@tanstack/ai-byokProviderId,byokHeaders, storage (memoryStoragedefault /passkeyStorage),validateKey,isPasskeyStorageSupported@tanstack/ai-byok/servergetByokKey(header-only, never logged),byokMissing(typed 401),scrubSecrets/maskKey@tanstack/ai-byok/react<ByokProvider storage={…}>,useByok()(withlocked/unlock), drop-in<ByokKeyManager>(last-4 display only)Storage — no plaintext
memoryStorage()(default) — session-only, nothing persisted, zero at-rest liability.passkeyStorage()(opt-in) — encrypted at rest: keyring in IndexedDB as AES-256-GCM ciphertext, key derived from a passkey's WebAuthn PRF output via HKDF, unwrapped on demand with a biometric/PIN tap. Fully client-side — no server, no custodian. Feature-detect withisPasskeyStorageSupported(), fall back to memory. Protects at-rest theft, not live in-page XSS (documented). Binds to the current origin by default (rpIdconfigurable). No plaintext persistence option by design.Example + E2E
examples/ts-react-chat):/byokroute (ByokProvider+ByokKeyManager+ chat sendingbyokHeaders) and/api/byok-chatrelay (getByokKey→ per-request adapter →byokMissing).testing/e2e/tests/byok.spec.ts): drives a real browser through the relay — asserts the key rides in the header and is absent from the request body, streams the aimock response, shows only last-4; and that a missing key surfacesbyokMissingwith no answer. Keyring is hydrated via a preloaded storage (the sameload()path passkey uses), so it's deterministic without a live WebAuthn ceremony.Tests
Unit:
byokHeaders,getByokKey,byokMissing, scrub, passkey AES-GCM round-trip + tamper,useByokset/clear +locked/unlock. Package gate green:test:types,test:eslint,test:lib(10),test:build/publint,build,test:sherif,test:knip,test:docs. E2E BYOK specs green.Follow-ups
docs/page.🤖 Generated with Claude Code
Summary by CodeRabbit
@tanstack/ai-byokfor end-to-end BYOK with React key management (session memory or optional passkey-encrypted storage), plus stateless server helpers.